package cn.song.freight.config;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.stereotype.Component;

import java.sql.Connection;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * MyBatis SQL打印拦截器
 * 用于监控和记录SQL执行情况，特别是慢SQL查询
 *
 * 主要功能：
 * 1. 记录SQL执行时间
 * 2. 打印完整的SQL语句（包含参数值）
 * 3. 标记执行时间超过5秒的慢SQL
 *
 * 注意：该插件对性能影响较小，打印复杂SQL的开销通常不超过30毫秒
 *
 * @author SongRenShuo
 * @date 2024/12/03
 */
@Slf4j
@Intercepts(value = {
        // 拦截prepare方法，获取更准确的SQL（解决复合操作时SQL不打印的问题）
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        // 拦截更新操作
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        // 拦截查询操作
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        // 拦截底层查询方法，确保不遗漏任何SQL
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
@Component
public class MybatisPrintSqlInterceptor implements Interceptor {

    /**
     * 慢SQL阈值：2秒
     * 执行时间超过此值的SQL将被记录
     */
    private static final long MAX_EXECUTION_TIME = 2000;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation == null) {
            return null;
        }

        // 1. 执行SQL并计时
        long proceedStart = System.currentTimeMillis();
        Object returnValue = null;
        Exception proceedException = null;
        try {
            returnValue = invocation.proceed();
        } catch (Exception e) {
            // 捕获异常但不处理，确保即使SQL执行失败也能打印完整SQL
            proceedException = e;
        }
        long proceedCost = System.currentTimeMillis() - proceedStart;

        // 如果执行时间在阈值内且没有异常，直接返回
        if (MAX_EXECUTION_TIME > proceedCost && proceedException == null) {
            return returnValue;
        }

        // 2. 打印SQL信息
        long printBegin = System.currentTimeMillis();

        // 获取SQL相关信息
        if (!(invocation.getArgs()[0] instanceof MappedStatement)) {
            return returnValue;
        }
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];

        // 获取BoundSql对象（包含SQL语句和参数信息）
        BoundSql boundSql = getBoundSql(invocation, mappedStatement);

        // 获取执行的方法名
        String statement = mappedStatement.getId();
        Configuration configuration = mappedStatement.getConfiguration();

        // 打印SQL信息
        showSql(configuration, boundSql, proceedCost, statement);

        long printEnd = System.currentTimeMillis();
        log.warn("SQL拦截器：打印SQL耗时：{} 毫秒", printEnd - printBegin);

        // 3. 如果有异常，继续抛出
        if (proceedException != null) {
            throw proceedException;
        }
        return returnValue;
    }

    /**
     * 获取BoundSql对象
     * 尝试从不同位置获取最准确的SQL信息
     */
    private BoundSql getBoundSql(Invocation invocation, MappedStatement mappedStatement) {
        // 首先从参数中查找BoundSql
        for (int i = invocation.getArgs().length - 1; i >= 0; i--) {
            if (invocation.getArgs()[i] instanceof BoundSql) {
                return (BoundSql) invocation.getArgs()[i];
            }
        }

        // 如果是RoutingStatementHandler，从其中获取BoundSql
        if (invocation.getTarget() instanceof RoutingStatementHandler) {
            return ((RoutingStatementHandler) invocation.getTarget()).getBoundSql();
        }

        // 最后从MappedStatement中获取
        Object parameter = invocation.getArgs().length > 1 ? invocation.getArgs()[1] : null;
        return mappedStatement.getBoundSql(parameter);
    }

    /**
     * 显示SQL执行信息
     */
    private void showSql(Configuration configuration, BoundSql boundSql, long elapsed, String statement) {
        String sql = getSqlWithValues(boundSql.getSql(), buildParameterValues(configuration, boundSql));
        String logText = formatMessage(elapsed, sql, statement);
        log.warn("SQL拦截器：{}", logText);
    }

    /**
     * 构建参数值映射
     * 将SQL参数转换为实际值
     */
    private static Map<Integer, Object> buildParameterValues(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        if (parameterMappings == null) {           
             return Collections.emptyMap();
        }
            Map<Integer, Object> parameterValues = new HashMap<>(parameterMappings.size());
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    // 获取参数值
                    Object value = getParameterValue(parameterMapping, parameterObject, boundSql, typeHandlerRegistry, configuration);
                    parameterValues.put(i, new Value(value));
                }
            }
            return parameterValues;
    }

    /**
     * 获取参数值
     */
    private static Object getParameterValue(ParameterMapping parameterMapping, Object parameterObject,
                                         BoundSql boundSql, TypeHandlerRegistry typeHandlerRegistry,
                                         Configuration configuration) {
        String propertyName = parameterMapping.getProperty();

        if (boundSql.hasAdditionalParameter(propertyName)) {
            return boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
            return null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            return parameterObject;
        } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            return metaObject.getValue(propertyName);
        }
    }

    /**
     * 格式化SQL日志消息
     */
    public static String formatMessage(long elapsed, String sql, String statement) {
        if (StringUtils.isBlank(sql)) {
            return "";
        }
        return String.format("执行SQL耗时：%d 毫秒%n执行方法：(%s)%n执行SQL：%n%s",
                elapsed, statement, sql.replace("\n", ""));
    }

    /**
     * 将SQL语句中的占位符替换为实际参数值
     */
    public static String getSqlWithValues(String statementQuery, Map<Integer, Object> parameterValues) {
        StringBuilder sb = new StringBuilder();
        int currentParameter = 0;

        for (int pos = 0; pos < statementQuery.length(); pos++) {
            char character = statementQuery.charAt(pos);
            if (character == '?' && currentParameter <= parameterValues.size()) {
                // 替换占位符为实际参数值
                Object value = parameterValues.get(currentParameter);
                sb.append(value != null ? value.toString() : new Value().toString());
                currentParameter++;
            } else {
                sb.append(character);
            }
        }
        return sb.toString();
    }

    /**
     * 值对象类
     * 用于处理不同类型参数的格式化输出
     */
    public static class Value {
        public static final String NORM_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
        private Object value;

        public Value(Object valueToSet) {
            this();
            this.value = valueToSet;
        }

        public Value() {
        }

        @Override
        public String toString() {
            return convertToString(this.value);
        }

        /**
         * 将各种类型的值转换为字符串
         */
        public String convertToString(Object value) {
            if (value == null) {
                return "NULL";
            }

            String result;
            if (value instanceof byte[]) {
                result = new String((byte[]) value);
            } else if (value instanceof Timestamp) {
                result = new SimpleDateFormat(NORM_DATETIME_PATTERN).format(value);
            } else if (value instanceof Date) {
                result = new SimpleDateFormat(NORM_DATETIME_PATTERN).format(value);
            } else if (value instanceof Boolean) {
                result = Boolean.FALSE.equals(value) ? "0" : "1";
            } else {
                result = value.toString();
            }

            return quoteIfNeeded(result, value);
        }

        /**
         * 为字符串类型的值添加引号
         */
        private String quoteIfNeeded(String stringValue, Object obj) {
            if (stringValue == null) {
                return null;
            }
            if (obj instanceof Number || obj instanceof Boolean) {
                return stringValue;
            }
            return "'" + escape(stringValue) + "'";
        }

        /**
         * 转义SQL字符串中的单引号
         */
        private String escape(String stringValue) {
            return stringValue.replaceAll("'", "''");
        }
    }
}
